package org.g4studio.core.orm.xibatis.sqlmap.engine.mapping.result;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.g4studio.core.orm.xibatis.common.beans.Probe;
import org.g4studio.core.orm.xibatis.common.beans.ProbeFactory;
import org.g4studio.core.orm.xibatis.common.jdbc.exception.NestedSQLException;
import org.g4studio.core.orm.xibatis.sqlmap.client.SqlMapException;
import org.g4studio.core.orm.xibatis.sqlmap.engine.exchange.DataExchange;
import org.g4studio.core.orm.xibatis.sqlmap.engine.impl.SqlMapClientImpl;
import org.g4studio.core.orm.xibatis.sqlmap.engine.impl.SqlMapExecutorDelegate;
import org.g4studio.core.orm.xibatis.sqlmap.engine.mapping.result.loader.ResultLoader;
import org.g4studio.core.orm.xibatis.sqlmap.engine.mapping.sql.Sql;
import org.g4studio.core.orm.xibatis.sqlmap.engine.mapping.statement.MappedStatement;
import org.g4studio.core.orm.xibatis.sqlmap.engine.scope.ErrorContext;
import org.g4studio.core.orm.xibatis.sqlmap.engine.scope.StatementScope;
import org.g4studio.core.orm.xibatis.sqlmap.engine.type.DomCollectionTypeMarker;
import org.g4studio.core.orm.xibatis.sqlmap.engine.type.DomTypeMarker;
import org.g4studio.core.orm.xibatis.sqlmap.engine.type.TypeHandler;
import org.g4studio.core.orm.xibatis.sqlmap.engine.type.TypeHandlerFactory;
import org.w3c.dom.Document;

/**
 * Basic implementation of ResultMap interface
 */
public class ResultMap {

	private static final Probe PROBE = ProbeFactory.getProbe();
	private static final String KEY_SEPARATOR = "\002";

	private String id;
	private Class resultClass;

	// DO NOT ACCESS EITHER OF THESE OUTSIDE OF THEIR BEAN GETTER/SETTER
	private ResultMapping[] resultMappings;
	private ThreadLocal remappableResultMappings = new ThreadLocal();

	private DataExchange dataExchange;

	private List nestedResultMappings;

	private Discriminator discriminator;

	private Set groupByProps;

	private String xmlName;

	private String resource;

	protected SqlMapExecutorDelegate delegate;

	protected boolean allowRemapping = false;
	public static final Object NO_VALUE = new Object();

	/**
	 * Constructor to pass a SqlMapExecutorDelegate in
	 * 
	 * @param delegate
	 *            - the SqlMapExecutorDelegate
	 */
	public ResultMap(SqlMapExecutorDelegate delegate) {
		this.delegate = delegate;
	}

	/**
	 * Getter for the SqlMapExecutorDelegate
	 * 
	 * @return - the delegate
	 */
	public SqlMapExecutorDelegate getDelegate() {
		return delegate;
	}

	public String getId() {
		return id;
	}

	/**
	 * Setter for the ID
	 * 
	 * @param id
	 *            - the new ID
	 */
	public void setId(String id) {
		this.id = id;
	}

	public Class getResultClass() {
		return resultClass;
	}

	public Object getUniqueKey(String keyPrefix, Object[] values) {
		if (groupByProps != null) {
			StringBuffer keyBuffer;
			if (keyPrefix != null)
				keyBuffer = new StringBuffer(keyPrefix);
			else
				keyBuffer = new StringBuffer();
			for (int i = 0; i < getResultMappings().length; i++) {
				String propertyName = getResultMappings()[i].getPropertyName();
				if (groupByProps.contains(propertyName)) {
					keyBuffer.append(values[i]);
					keyBuffer.append('-');
				}
			}
			if (keyBuffer.length() < 1) {
				return null;
			} else {
				// seperator value not likely to appear in a database
				keyBuffer.append(KEY_SEPARATOR);
				return keyBuffer.toString();
			}
		} else {
			return null;
		}
	}

	public Object getUniqueKey(Object[] values) {
		return getUniqueKey(null, values);
	}

	/**
	 * Setter for the result class (what the results will be mapped into)
	 * 
	 * @param resultClass
	 *            - the result class
	 */
	public void setResultClass(Class resultClass) {
		this.resultClass = resultClass;
	}

	/**
	 * Getter for the DataExchange object to be used
	 * 
	 * @return - the DataExchange object
	 */
	public DataExchange getDataExchange() {
		return dataExchange;
	}

	/**
	 * Setter for the DataExchange object to be used
	 * 
	 * @param dataExchange
	 *            - the new DataExchange object
	 */
	public void setDataExchange(DataExchange dataExchange) {
		this.dataExchange = dataExchange;
	}

	/**
	 * Getter (used by DomDataExchange) for the xml name of the results
	 * 
	 * @return - the name
	 */
	public String getXmlName() {
		return xmlName;
	}

	/**
	 * Setter (used by the SqlMapBuilder) for the xml name of the results
	 * 
	 * @param xmlName
	 *            - the name
	 */
	public void setXmlName(String xmlName) {
		this.xmlName = xmlName;
	}

	/**
	 * Getter for the resource (used to report errors)
	 * 
	 * @return - the resource
	 */
	public String getResource() {
		return resource;
	}

	/**
	 * Setter for the resource (used by the SqlMapBuilder)
	 * 
	 * @param resource
	 *            - the resource name
	 */
	public void setResource(String resource) {
		this.resource = resource;
	}

	public void addGroupByProperty(String name) {
		if (groupByProps == null) {
			groupByProps = new HashSet();
		}
		groupByProps.add(name);
	}

	public boolean hasGroupBy() {
		return groupByProps != null && groupByProps.size() > 0;
	}

	public Iterator groupByProps() {
		return groupByProps.iterator();
	}

	public void addNestedResultMappings(ResultMapping mapping) {
		if (nestedResultMappings == null) {
			nestedResultMappings = new ArrayList();
		}
		nestedResultMappings.add(mapping);
	}

	public List getNestedResultMappings() {
		return nestedResultMappings;
	}

	public ResultMapping[] getResultMappings() {
		if (allowRemapping) {
			return (ResultMapping[]) remappableResultMappings.get();
		} else {
			return resultMappings;
		}
	}

	public void setDiscriminator(Discriminator discriminator) {
		if (this.discriminator != null) {
			throw new SqlMapException("A discriminator may only be set once per result map.");
		}
		this.discriminator = discriminator;
	}

	public Discriminator getDiscriminator() {
		return discriminator;
	}

	public ResultMap resolveSubMap(StatementScope statementScope, ResultSet rs) throws SQLException {
		ResultMap subMap = this;
		if (discriminator != null) {
			ResultMapping mapping = (ResultMapping) discriminator.getResultMapping();
			Object value = getPrimitiveResultMappingValue(rs, mapping);
			if (value == null) {
				value = doNullMapping(value, mapping);
			}
			subMap = discriminator.getSubMap(String.valueOf(value));
			if (subMap == null) {
				subMap = this;
			} else if (subMap != this) {
				subMap = subMap.resolveSubMap(statementScope, rs);
			}
		}
		return subMap;
	}

	/**
	 * Setter for a list of the individual ResultMapping objects
	 * 
	 * @param resultMappingList
	 *            - the list
	 */
	public void setResultMappingList(List resultMappingList) {
		if (allowRemapping) {
			this.remappableResultMappings.set((ResultMapping[]) resultMappingList
					.toArray(new ResultMapping[resultMappingList.size()]));
		} else {
			this.resultMappings = (ResultMapping[]) resultMappingList.toArray(new ResultMapping[resultMappingList
					.size()]);
		}

		Map props = new HashMap();
		props.put("map", this);
		dataExchange = getDelegate().getDataExchangeFactory().getDataExchangeForClass(resultClass);
		dataExchange.initialize(props);
	}

	/**
	 * Getter for the number of ResultMapping objects
	 * 
	 * @return - the count
	 */
	public int getResultCount() {
		return this.getResultMappings().length;
	}

	/**
	 * Read a row from a resultset and map results to an array.
	 * 
	 * @param statementScope
	 *            scope of the request
	 * @param rs
	 *            ResultSet to read from
	 * 
	 * @return row read as an array of column values.
	 * 
	 * @throws java.sql.SQLException
	 */
	public Object[] getResults(StatementScope statementScope, ResultSet rs) throws SQLException {
		ErrorContext errorContext = statementScope.getErrorContext();
		errorContext.setActivity("applying a result map");
		errorContext.setObjectId(this.getId());
		errorContext.setResource(this.getResource());
		errorContext.setMoreInfo("Check the result map.");

		boolean foundData = false;
		Object[] columnValues = new Object[getResultMappings().length];
		for (int i = 0; i < getResultMappings().length; i++) {
			ResultMapping mapping = (ResultMapping) getResultMappings()[i];
			errorContext.setMoreInfo(mapping.getErrorString());
			if (mapping.getStatementName() != null) {
				if (resultClass == null) {
					throw new SqlMapException(
							"The result class was null when trying to get results for ResultMap named " + getId() + ".");
				} else if (Map.class.isAssignableFrom(resultClass)) {
					Class javaType = mapping.getJavaType();
					if (javaType == null) {
						javaType = Object.class;
					}
					columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, javaType);
				} else if (DomTypeMarker.class.isAssignableFrom(resultClass)) {
					Class javaType = mapping.getJavaType();
					if (javaType == null) {
						javaType = DomTypeMarker.class;
					}
					columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, javaType);
				} else {
					Probe p = ProbeFactory.getProbe(resultClass);
					Class type = p.getPropertyTypeForSetter(resultClass, mapping.getPropertyName());
					columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, type);
				}
				foundData = foundData || columnValues[i] != null;
			} else if (mapping.getNestedResultMapName() == null) {
				columnValues[i] = getPrimitiveResultMappingValue(rs, mapping);
				if (columnValues[i] == null) {
					columnValues[i] = doNullMapping(columnValues[i], mapping);
				} else {
					foundData = true;
				}
			}
		}

		statementScope.setRowDataFound(foundData);

		return columnValues;
	}

	public Object setResultObjectValues(StatementScope statementScope, Object resultObject, Object[] values) {
		final String previousNestedKey = statementScope.getCurrentNestedKey();
		String ukey = (String) getUniqueKey(statementScope.getCurrentNestedKey(), values);
		Map uniqueKeys = statementScope.getUniqueKeys(this);
		statementScope.setCurrentNestedKey(ukey);
		if (uniqueKeys != null && uniqueKeys.containsKey(ukey)) {
			// Unique key is already known, so get the existing result object
			// and process additional results.
			resultObject = uniqueKeys.get(ukey);
			applyNestedResultMap(statementScope, resultObject, values);
			resultObject = NO_VALUE;
		} else if (ukey == null || uniqueKeys == null || !uniqueKeys.containsKey(ukey)) {
			// Unique key is NOT known, so create a new result object and then
			// process additional results.
			resultObject = dataExchange.setData(statementScope, this, resultObject, values);
			// Lazy init key set, only if we're grouped by something (i.e. ukey
			// != null)
			if (ukey != null) {
				if (uniqueKeys == null) {
					uniqueKeys = new HashMap();
					statementScope.setUniqueKeys(this, uniqueKeys);
				}
				uniqueKeys.put(ukey, resultObject);
			}
			applyNestedResultMap(statementScope, resultObject, values);
		} else {
			// Otherwise, we don't care about these results.
			resultObject = NO_VALUE;
		}

		statementScope.setCurrentNestedKey(previousNestedKey);
		return resultObject;
	}

	private void applyNestedResultMap(StatementScope statementScope, Object resultObject, Object[] values) {
		if (resultObject != null && resultObject != NO_VALUE) {
			if (nestedResultMappings != null) {
				for (int i = 0, n = nestedResultMappings.size(); i < n; i++) {
					ResultMapping resultMapping = (ResultMapping) nestedResultMappings.get(i);
					setNestedResultMappingValue(resultMapping, statementScope, resultObject, values);
				}
			}
		}
	}

	/**
	 * Some changes in this method for IBATIS-225:
	 * <ul>
	 * <li>We no longer require the nested property to be a collection. This
	 * will allow reuses of resultMaps on 1:1 relationships</li>
	 * <li>If the nested property is not a collection, then it will be
	 * created/replaced by the values generated from the current row.</li>
	 * </ul>
	 * 
	 * @param mapping
	 * @param statementScope
	 * @param resultObject
	 * @param values
	 */
	protected void setNestedResultMappingValue(ResultMapping mapping, StatementScope statementScope,
			Object resultObject, Object[] values) {
		try {

			String resultMapName = mapping.getNestedResultMapName();
			ResultMap resultMap = getDelegate().getResultMap(resultMapName);
			// get the discriminated submap if it exists
			resultMap = resultMap.resolveSubMap(statementScope, statementScope.getResultSet());

			Class type = mapping.getJavaType();
			String propertyName = mapping.getPropertyName();

			Object obj = PROBE.getObject(resultObject, propertyName);

			if (obj == null) {
				if (type == null) {
					type = PROBE.getPropertyTypeForSetter(resultObject, propertyName);
				}

				try {
					// create the object if is it a Collection. If not a
					// Collection
					// then we will just set the property to the object created
					// in processing the nested result map
					if (Collection.class.isAssignableFrom(type)) {
						obj = ResultObjectFactoryUtil.createObjectThroughFactory(type);
						PROBE.setObject(resultObject, propertyName, obj);
					}
				} catch (Exception e) {
					throw new SqlMapException("Error instantiating collection property for mapping '"
							+ mapping.getPropertyName() + "'.  Cause: " + e, e);
				}
			}

			// JIRA 375
			// "Provide a way for not creating items from nested ResultMaps when the items contain only null values"
			boolean subResultObjectAbsent = false;
			if (mapping.getNotNullColumn() != null) {
				if (statementScope.getResultSet().getObject(mapping.getNotNullColumn()) == null) {
					subResultObjectAbsent = true;
				}
			}
			if (!subResultObjectAbsent) {
				values = resultMap.getResults(statementScope, statementScope.getResultSet());
				if (statementScope.isRowDataFound()) {
					Object o = resultMap.setResultObjectValues(statementScope, null, values);
					if (o != NO_VALUE) {
						if (obj != null && obj instanceof Collection) {
							((Collection) obj).add(o);
						} else {
							PROBE.setObject(resultObject, propertyName, o);
						}
					}
				}
			}
		} catch (SQLException e) {
			throw new SqlMapException("Error getting nested result map values for '" + mapping.getPropertyName()
					+ "'.  Cause: " + e, e);
		}
	}

	protected Object getNestedSelectMappingValue(StatementScope statementScope, ResultSet rs, ResultMapping mapping,
			Class targetType) throws SQLException {
		try {
			TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory();

			String statementName = mapping.getStatementName();
			SqlMapClientImpl client = (SqlMapClientImpl) statementScope.getSession().getSqlMapClient();

			MappedStatement mappedStatement = client.getMappedStatement(statementName);
			Class parameterType = mappedStatement.getParameterClass();
			Object parameterObject = null;

			if (parameterType == null) {
				parameterObject = prepareBeanParameterObject(statementScope, rs, mapping, parameterType);
			} else {
				if (typeHandlerFactory.hasTypeHandler(parameterType)) {
					parameterObject = preparePrimitiveParameterObject(rs, mapping, parameterType);
				} else if (DomTypeMarker.class.isAssignableFrom(parameterType)) {
					parameterObject = prepareDomParameterObject(rs, mapping);
				} else {
					parameterObject = prepareBeanParameterObject(statementScope, rs, mapping, parameterType);
				}
			}

			Object result = null;
			if (parameterObject != null) {

				Sql sql = mappedStatement.getSql();
				ResultMap resultMap = sql.getResultMap(statementScope, parameterObject);
				Class resultClass = resultMap.getResultClass();

				if (resultClass != null && !DomTypeMarker.class.isAssignableFrom(targetType)) {
					if (DomCollectionTypeMarker.class.isAssignableFrom(resultClass)) {
						targetType = DomCollectionTypeMarker.class;
					} else if (DomTypeMarker.class.isAssignableFrom(resultClass)) {
						targetType = DomTypeMarker.class;
					}
				}

				result = ResultLoader.loadResult(client, statementName, parameterObject, targetType);

				String nullValue = mapping.getNullValue();
				if (result == null && nullValue != null) {
					TypeHandler typeHandler = typeHandlerFactory.getTypeHandler(targetType);
					if (typeHandler != null) {
						result = typeHandler.valueOf(nullValue);
					}
				}
			}
			return result;
		} catch (InstantiationException e) {
			throw new NestedSQLException("Error setting nested bean property.  Cause: " + e, e);
		} catch (IllegalAccessException e) {
			throw new NestedSQLException("Error setting nested bean property.  Cause: " + e, e);
		}

	}

	private Object preparePrimitiveParameterObject(ResultSet rs, ResultMapping mapping, Class parameterType)
			throws SQLException {
		Object parameterObject;
		TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory();
		TypeHandler th = typeHandlerFactory.getTypeHandler(parameterType);
		parameterObject = th.getResult(rs, mapping.getColumnName());
		return parameterObject;
	}

	private Document newDocument(String root) {
		try {
			Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
			doc.appendChild(doc.createElement(root));
			return doc;
		} catch (ParserConfigurationException e) {
			throw new RuntimeException("Error creating XML document.  Cause: " + e);
		}
	}

	private Object prepareDomParameterObject(ResultSet rs, ResultMapping mapping) throws SQLException {
		TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory();

		Document doc = newDocument("parameter");
		Probe probe = ProbeFactory.getProbe(doc);

		String complexName = mapping.getColumnName();

		TypeHandler stringTypeHandler = typeHandlerFactory.getTypeHandler(String.class);
		if (complexName.indexOf('=') > -1) {
			// old 1.x style multiple params
			StringTokenizer parser = new StringTokenizer(complexName, "{}=, ", false);
			while (parser.hasMoreTokens()) {
				String propName = parser.nextToken();
				String colName = parser.nextToken();
				Object propValue = stringTypeHandler.getResult(rs, colName);
				probe.setObject(doc, propName, propValue.toString());
			}
		} else {
			// single param
			Object propValue = stringTypeHandler.getResult(rs, complexName);
			probe.setObject(doc, "value", propValue.toString());
		}

		return doc;
	}

	private Object prepareBeanParameterObject(StatementScope statementScope, ResultSet rs, ResultMapping mapping,
			Class parameterType) throws InstantiationException, IllegalAccessException, SQLException {
		TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory();

		Object parameterObject;
		if (parameterType == null) {
			parameterObject = new HashMap();
		} else {
			parameterObject = ResultObjectFactoryUtil.createObjectThroughFactory(parameterType);
		}
		String complexName = mapping.getColumnName();

		if (complexName.indexOf('=') > -1 || complexName.indexOf(',') > -1) {
			StringTokenizer parser = new StringTokenizer(complexName, "{}=, ", false);
			while (parser.hasMoreTokens()) {
				String propName = parser.nextToken();
				String colName = parser.nextToken();
				Class propType = PROBE.getPropertyTypeForSetter(parameterObject, propName);
				TypeHandler propTypeHandler = typeHandlerFactory.getTypeHandler(propType);
				Object propValue = propTypeHandler.getResult(rs, colName);
				PROBE.setObject(parameterObject, propName, propValue);
			}
		} else {
			// single param
			TypeHandler propTypeHandler = typeHandlerFactory.getTypeHandler(parameterType);
			if (propTypeHandler == null) {
				propTypeHandler = typeHandlerFactory.getUnkownTypeHandler();
			}
			parameterObject = propTypeHandler.getResult(rs, complexName);
		}

		return parameterObject;
	}

	protected Object getPrimitiveResultMappingValue(ResultSet rs, ResultMapping mapping) throws SQLException {
		Object value = null;
		TypeHandler typeHandler = mapping.getTypeHandler();
		if (typeHandler != null) {
			String columnName = mapping.getColumnName();
			int columnIndex = mapping.getColumnIndex();
			if (columnName == null) {
				value = typeHandler.getResult(rs, columnIndex);
			} else {
				value = typeHandler.getResult(rs, columnName);
			}
		} else {
			throw new SqlMapException("No type handler could be found to map the property '"
					+ mapping.getPropertyName() + "' to the column '" + mapping.getColumnName()
					+ "'.  One or both of the types, or the combination of types is not supported.");
		}
		return value;
	}

	protected Object doNullMapping(Object value, ResultMapping mapping) throws SqlMapException {
		if (value == null) {
			TypeHandler typeHandler = mapping.getTypeHandler();
			if (typeHandler != null) {
				String nullValue = mapping.getNullValue();
				if (nullValue != null)
					value = typeHandler.valueOf(nullValue);
				return value;
			} else {
				throw new SqlMapException("No type handler could be found to map the property '"
						+ mapping.getPropertyName() + "' to the column '" + mapping.getColumnName()
						+ "'.  One or both of the types, or the combination of types is not supported.");
			}
		} else {
			return value;
		}
	}
}
